package com.toplabs.unashu.interceptor;

import cn.hutool.core.map.MapUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.RSA;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.json.JSONException;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.core.type.TypeReference;
import com.toplabs.unashu.annotation.ApiSafe;
import com.toplabs.unashu.common.Constants;
import com.toplabs.unashu.common.SafeType;
import com.toplabs.unashu.common.response.ResponseStatus;
import com.toplabs.unashu.common.utils.ApiUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;

/**
 * @author toplabs
 * @since 2022/08/09
 * <p>This java file was created by toplabs in 2022/08/09.
 * The following is the description information about this file:</p>
 * <p>description: api安全拦截器</p>
 */
@Slf4j
@Component
public class ApiSafeInterceptor implements HandlerInterceptor {

    @Value("${api.safe.secret}")
    private String secret;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod method = (HandlerMethod) handler;
        // 方法上是否有注解
        ApiSafe type = method.getMethodAnnotation(ApiSafe.class);
        if (type == null) { // 类上是否有注解
            type = method.getBeanType().getAnnotation(ApiSafe.class);
            if (null == type) {
                return true;
            }
        }
        String accessToken = request.getHeader(Constants.HEADER_KEY_AUTHORIZATION);
        String timestamp = request.getHeader(Constants.HEADER_KEY_TIMESTAMP);
        String nonce = request.getHeader(Constants.HEADER_KEY_NONCE);
        String sign = request.getHeader(Constants.HEADER_KEY_SIGN);

        // 判断需要的参数是否为空，不为空进行后续操作
        if (CharSequenceUtil.isEmpty(sign)) {
            ApiUtils.result(response, ResponseStatus.VALIDATE_FAILED.getCode(), "[illegal parameter], sign cannot be empty");
            return false;
        }
        // 判断时间戳是否为空
        if (CharSequenceUtil.isEmpty(timestamp)) {
            ApiUtils.result(response, ResponseStatus.VALIDATE_FAILED.getCode(), "[illegal parameter], timestamp cannot be empty");
            return false;
        }
        // 判断是否需要验证签名
        if (SafeType.SIGN.equals(type.value())) {
            String body = ApiUtils.convertToString(request.getInputStream());
            if (CharSequenceUtil.isNotBlank(body)) {
                try {
                    body = JSONUtil.parseObj(body).toString();
                } catch (JSONException e) {
                    log.error("json parsing failed: ", e);
                }
            }
            Map<String, String> params = ApiUtils.getUrlParams(request);
            params.put(Constants.API_SAFE_BODY_KEY, body);
            String signStr = MapUtil.sortJoin(params, "&", CharSequenceUtil.EMPTY, false, nonce, timestamp, secret, CharSequenceUtil.emptyIfNull(accessToken));
            String signature = DigestUtil.md5Hex(signStr).toUpperCase();
            if (!sign.equals(signature)) {
                log.error("Invalid signature");
                log.error("body: {}", body);
                log.error("sign: {}", sign);
                log.error("signature: {}", signature);
                ApiUtils.result(response, ResponseStatus.VALIDATE_FAILED_SIGN);
                return false;
            }
        }
        return true;
    }

    /**
     * 解密request封装
     *
     * @author ldy
     */
    @SuppressWarnings("unused")
    private static class DecryptRequest extends HttpServletRequestWrapper {

        // 所有参数的Map集合
        private Map<String, String[]> parameterMap;
        // 输入流
        private InputStream inputStream;

        public DecryptRequest(HttpServletRequest request) throws IOException {
            super(request);
            String contentType = request.getHeader("Content-Type");
            log.debug("DecryptRequest -> contentType:{}", contentType);
            String encrypt = null;
            if (null != contentType && contentType.contains(Constants.APPLICATION_JSON)) {
                // json
                String body = ApiUtils.convertToString(request.getInputStream());
                if (StringUtils.hasText(body)) {
                    encrypt = JSONUtil.parseObj(body).getStr("encrypt");
                }
            } else {
                // url
                encrypt = request.getParameter("encrypt");
            }
            log.debug("DecryptRequest -> encrypt:{}", encrypt);
            // 解密
            String params = decrypt(encrypt);
            // 解密失败
            if (!StringUtils.hasText(params)) {
                request.setAttribute("code", 1009);
                request.setAttribute("msg", "服务器拒绝请求");
                return;
            }
            if (null != contentType && contentType.contains(Constants.APPLICATION_JSON) && this.inputStream == null) {
                this.inputStream = new DecryptInputStream(new ByteArrayInputStream(params.getBytes()));
            }
            parameterMap = buildParams(params);
        }

        private String decrypt(String encrypt) {
            try {
                // 解密
                final RSA rsa = new RSA();
                return StrUtil.str(rsa.decrypt(encrypt, KeyType.PrivateKey), CharsetUtil.CHARSET_UTF_8);
            } catch (Exception e) {
                return null;
            }
        }

        private Map<String, String[]> buildParams(String src) {
            Map<String, String[]> map = new HashMap<>();
            Map<String, String> params = JSONUtil.toBean(src, TypeReference.class, false);
            for (Map.Entry<String, String> entry : params.entrySet()) {
                map.put(entry.getKey(), new String[]{entry.getValue()});
            }
            return map;
        }

        @Override
        public String getParameter(String name) {
            String[] values = getParameterMap().get(name);
            if (values != null) {
                return (values.length > 0 ? values[0] : null);
            }
            return super.getParameter(name);
        }

        @Override
        public String[] getParameterValues(String name) {
            String[] values = getParameterMap().get(name);
            if (values != null) {
                return values;
            }
            return super.getParameterValues(name);
        }

        @Override
        public Enumeration<String> getParameterNames() {
            Map<String, String[]> multipartParameters = getParameterMap();
            if (multipartParameters.isEmpty()) {
                return super.getParameterNames();
            }

            Set<String> paramNames = new LinkedHashSet<>();
            Enumeration<String> paramEnum = super.getParameterNames();
            while (paramEnum.hasMoreElements()) {
                paramNames.add(paramEnum.nextElement());
            }
            paramNames.addAll(multipartParameters.keySet());
            return Collections.enumeration(paramNames);
        }

        @Override
        public Map<String, String[]> getParameterMap() {
            return null == parameterMap ? super.getParameterMap() : parameterMap;
        }

        @Override
        public ServletInputStream getInputStream() throws IOException {
            return this.inputStream == null ? super.getInputStream() : (ServletInputStream) this.inputStream;
        }
    }

    /**
     * 自定义ServletInputStream
     *
     * @author toplabs
     */
    private static class DecryptInputStream extends ServletInputStream {

        private final InputStream sourceStream;

        /**
         * Create a DelegatingServletInputStream for the given source stream.
         *
         * @param sourceStream the source stream (never {@code null})
         */
        public DecryptInputStream(InputStream sourceStream) {
            Assert.notNull(sourceStream, "Source InputStream must not be null");
            this.sourceStream = sourceStream;
        }

        @Override
        public int read() throws IOException {
            return this.sourceStream.read();
        }

        @Override
        public void close() throws IOException {
            super.close();
            this.sourceStream.close();
        }

        @Override
        public boolean isFinished() {
            return false;
        }

        @Override
        public boolean isReady() {
            return false;
        }

        @Override
        public void setReadListener(ReadListener readListener) {
            // set ReadListener
        }
    }
}
